Skip to content

[blueprint + dart] Scope validator to single catalog. - #2538

Open
polina-c wants to merge 2 commits into
mainfrom
core-follow-up
Open

[blueprint + dart] Scope validator to single catalog.#2538
polina-c wants to merge 2 commits into
mainfrom
core-follow-up

Conversation

@polina-c

@polina-c polina-c commented Sep 5, 2026

Copy link
Copy Markdown
Collaborator

Contributes to #2356, #2373.

Follow up for: #2439 (comment)

What this PR changes

A2uiValidator holds one catalog instead of a map of them:

-  final Map<String, Catalog<C, F>> catalogs;
+  final Catalog<C, F> catalog;

A component belongs to exactly one catalog, and v0.9 declares catalogId on createSurface alone, so a validator holding several cannot tell which one applies to a payload that only updates a surface. #2439 worked around that with a surfaceCatalogs map threaded through validate, validateStructure and validateAgainstCatalogs, plus a _catalogFor ladder that fell back to the sole catalog and otherwise threw. Scoping the validator to one catalog removes the question, and all of that with it. validateComponent, validateTheme and validateComponentBatch lose their Catalog parameter for the same reason. A payload that creates a surface against any other catalog now raises A2uiCatalogError.

MessageProcessor keeps one validator per catalog, built on first use and reused, reachable through validatorFor(catalog). Each SurfaceModel already recorded the catalog it was created with, so every check routes to the validator for that catalog. The validator: constructor parameter is replaced by protocolVersion and commonTypesSchema: injecting a single validator means nothing once there are several, and taking the configuration directly keeps the version consistent between envelope parsing and catalog checks.

Envelope parsing moves to A2uiValidator.parseMessagesFor, a static that takes no catalog. The version tag and the single-update-type rule read no catalog, so a renderer can parse a payload before matching each message to a surface, and so to a catalog. processPayload parses this way, then dispatches.

On the "block mixed update types" prerequisite

#2439 (comment) asks for this first. Dart already enforces it, in A2uiMessage.fromJson, which throws A2uiValidationError when an envelope carries more than one of createSurface, updateComponents, updateDataModel, deleteSurface. processMessages takes sealed typed messages, so it cannot mix by construction. Nothing pinned the rule, so this adds a test.

What actually had to change first was making the envelope stage catalog-independent, since that is the step that has to happen before a message can be matched to its catalog. Batch-level grouping by surface turned out to be unnecessary: the processor dispatches per message, and each message names one surface, hence one catalog.

Tests

processPayload rejects an envelope mixing update types pins the rule above.

MessageProcessor catalog scope pins per-surface catalog isolation, which nothing covered:

s1 (cat1) + Alpha: ACCEPTED     s2 (cat2) + Beta:  ACCEPTED
s1 (cat1) + Beta:  REJECTED     s2 (cat2) + Alpha: REJECTED

plus that validatorFor returns the same instance per catalog, since the schema cache depends on it.

A2uiValidator catalog scope replaces the surfaceCatalogs group: an incremental payload is checked against the validator's catalog rather than skipped, a component from another catalog is rejected, and a createSurface naming another catalog raises A2uiCatalogError from both validate and validateStructure.

The conformance harness derives one catalog per case instead of registering the document under every id the payload names.

Blueprint

New "Catalog Scope" section under the validation layer, written as instructions for implementers in other languages: scope the validator to one catalog, expose catalog-free envelope parsing, build one validator per catalog in MessageProcessor and reuse it, record the catalog on the surface, check each surface against the catalog it was created with. The A2uiValidator signature, the renderer/agent table and the validation matrix are updated, with two new Catalog Scope rows.

Verification

  • dart analyze in dart/a2ui_core and dart/a2ui_agent: no issues
  • dart test in dart/a2ui_core: 297 passed, 25 skipped (pre-existing v0.8 cases)
  • dart format: clean

@github-project-automation github-project-automation Bot moved this to Todo in A2UI Sep 5, 2026
@polina-c polina-c changed the title Scope validator to single catalog. [blueprint + dart] Scope validator to single catalog. Sep 5, 2026

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request refactors the validation logic to scope each A2uiValidator to a single catalog rather than a list of catalogs, updating MessageProcessor to manage and cache a validator per catalog. This ensures components are strictly validated against their surface's specific catalog. A review comment correctly notes an inconsistency between the blueprint matrix, which references A2uiValidator.parseMessages(), and the Dart implementation, which introduces parseMessagesFor as the static method.

Comment on lines +511 to +512
| **Protocol Envelope** | Single update type per message (`createSurface`, `updateComponents`, etc.) | `A2uiValidator.parseMessages()` (envelope schema, no catalog needed) | `A2uiValidationError` |
| **Protocol Envelope** | Valid `version` tag (`v0.8`, `v0.9`, `v1.0`) & required envelope keys | `A2uiValidator.parseMessages()` (envelope schema, no catalog needed) | `A2uiValidationError` |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The blueprint matrix refers to 'A2uiValidator.parseMessages()' for envelope validation, but the static method added in the Dart implementation is named 'parseMessagesFor' (and 'parseMessages' is kept as an instance method). Consider aligning the blueprint or documenting this language-specific naming difference to avoid confusion.

@polina-c polina-c Sep 5, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment suggests a fresh validator per batch. This memoizes one per catalog id instead. The validator caches its catalog's resolved component schemas, and resolveSchemaRefs runs over every component in the catalog; a fresh instance per batch would rebuild that on every streamed update. Same scoping, no per-message rebuild.

As schemas are not supposed to change in runtime, caching is ok.

@polina-c
polina-c requested a review from nan-yu September 5, 2026 17:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant